<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="../lib/bind/accessors.html">
<link rel="import" href="../lib/bind/effects.html">
<link rel="import" href="../lib/path.html">
<script>
  /**
   * Support for property side effects.
   *
   * Key for effect objects:
   *
   * property | ann | anCmp | cmp | obs | cplxOb | description
   * ---------|-----|-------|-----|-----|--------|----------------------------------------
   * method   |     | X     | X   | X   | X      | function name to call on instance
   * args     |     | X     | X   |     | X      | arg descriptors for triggers of fn
   * trigger  |     | X     | X   |     | X      | describes triggering dependency (one of args)
   * property |     |       | X   | X   |        | property for effect to set or get
   * name     | X   |       |     |     |        | annotation value (text inside {{...}})
   * kind     | X   | X     |     |     |        | binding type (property or attribute)
   * index    | X   | X     |     |     |        | node index to set
   *
   */

  Polymer.Base._addFeature({

    _addPropertyEffect: function(property, kind, effect) {
      var prop = Polymer.Bind.addPropertyEffect(this, property, kind, effect);
      // memoize path function for faster lookup.
      prop.pathFn = this['_' + prop.kind + 'PathEffect'];
    },

    // prototyping

    _prepEffects: function() {
      Polymer.Bind.prepareModel(this);
      this._addAnnotationEffects(this._notes);
    },

    _prepBindings: function() {
      Polymer.Bind.createBindings(this);
    },

    _addPropertyEffects: function(properties) {
      if (properties) {
        for (var p in properties) {
          var prop = properties[p];
          if (prop.observer) {
            this._addObserverEffect(p, prop.observer);
          }
          if (prop.computed) {
            // Computed properties are implicitly readOnly
            prop.readOnly = true;
            this._addComputedEffect(p, prop.computed);
          }
          if (prop.notify) {
            this._addPropertyEffect(p, 'notify', {
              event: Polymer.CaseMap.camelToDashCase(p) + '-changed'});
          }
          if (prop.reflectToAttribute) {
            var attr = Polymer.CaseMap.camelToDashCase(p);
            if (attr[0] === '-') {
              this._warn(this._logf('_addPropertyEffects', 'Property ' + p + ' cannot be reflected to attribute ' + attr + ' because "-" is not a valid starting attribute name. Use a lowercase first letter for the property instead.'));
            } else {
              this._addPropertyEffect(p, 'reflect', {
                attribute: attr
              });
            }
          }
          if (prop.readOnly) {
            // Ensure accessor is created
            Polymer.Bind.ensurePropertyEffects(this, p);
          }
        }
      }
    },

    _addComputedEffect: function(name, expression) {
      var sig = this._parseMethod(expression);

      var dynamicFn = sig.dynamicFn;

      for (var i=0, arg; (i<sig.args.length) && (arg=sig.args[i]); i++) {
        this._addPropertyEffect(arg.model, 'compute', {
          method: sig.method,
          args: sig.args,
          trigger: arg,
          name: name,
          dynamicFn: dynamicFn
        });
      }
      if (dynamicFn) {
        this._addPropertyEffect(sig.method, 'compute', {
          method: sig.method,
          args: sig.args,
          trigger: null,
          name: name,
          dynamicFn: dynamicFn
        });
      }
    },

    _addObserverEffect: function(property, observer) {
      this._addPropertyEffect(property, 'observer', {
        method: observer,
        property: property
      });
    },

    _addComplexObserverEffects: function(observers) {
      if (observers) {
        for (var i=0, o; (i<observers.length) && (o=observers[i]); i++)  {
          this._addComplexObserverEffect(o);
        }
      }
    },

    _addComplexObserverEffect: function(observer) {
      var sig = this._parseMethod(observer);

      if (!sig) {
        throw new Error("Malformed observer expression '" + observer + "'");
      }

      var dynamicFn = sig.dynamicFn;

      for (var i=0, arg; (i<sig.args.length) && (arg=sig.args[i]); i++) {
        this._addPropertyEffect(arg.model, 'complexObserver', {
          method: sig.method,
          args: sig.args,
          trigger: arg,
          dynamicFn: dynamicFn
        });
      }
      if (dynamicFn) {
        this._addPropertyEffect(sig.method, 'complexObserver', {
          method: sig.method,
          args: sig.args,
          trigger: null,
          dynamicFn: dynamicFn
        });
      }
    },

    _addAnnotationEffects: function(notes) {
      // process annotations that have been parsed from template
      for (var i=0, note; (i<notes.length) && (note=notes[i]); i++)  {
        // where to find the node in the concretized list
        var b$ = note.bindings;
        for (var j=0, binding; (j<b$.length) && (binding=b$[j]); j++) {
          this._addAnnotationEffect(binding, i);
        }
      }
    },

    _addAnnotationEffect: function(note, index) {
      // TODO(sjmiles): annotations have 'effects' proper and 'listener'
      if (Polymer.Bind._shouldAddListener(note)) {
        // <node>.on.<dash-case-property>-changed: <path> = e.detail.value
        Polymer.Bind._addAnnotatedListener(this, index,
          note.name, note.parts[0].value, note.parts[0].event, note.parts[0].negate);
      }
      for (var i=0; i<note.parts.length; i++) {
        var part = note.parts[i];
        if (part.signature) {
          this._addAnnotatedComputationEffect(note, part, index);
        } else if (!part.literal) {
          // add 'annotation' binding effect for property 'model'
          if (note.kind === 'attribute' && note.name[0] === '-') {
            this._warn(this._logf('_addAnnotationEffect', 'Cannot set attribute ' + note.name + ' because "-" is not a valid attribute starting character'));
          } else {
            this._addPropertyEffect(part.model, 'annotation', {
              kind: note.kind,
              index: index,
              name: note.name,
              propertyName: note.propertyName,
              value: part.value,
              isCompound: note.isCompound,
              compoundIndex: part.compoundIndex,
              event: part.event,
              customEvent: part.customEvent,
              negate: part.negate
            });
          }
        }
      }
    },

    _addAnnotatedComputationEffect: function(note, part, index) {
      var sig = part.signature;
      if (sig.static) {
        this.__addAnnotatedComputationEffect('__static__', index, note, part, null);
      } else {
        for (var i=0, arg; (i<sig.args.length) && (arg=sig.args[i]); i++) {
          if (!arg.literal) {
            this.__addAnnotatedComputationEffect(arg.model, index, note, part,
              arg);
          }
        }
        if (sig.dynamicFn) {
          // trigger=null is sufficient as long as we don't allow paths to be
          // used. If we change our mind, we must first implement this in the
          // effects anyway where we basically do a `fn = this[methodName]` at
          // the moment.
          this.__addAnnotatedComputationEffect(
              sig.method, index, note, part, null);
        }
      }
    },

    __addAnnotatedComputationEffect: function(property, index, note, part, trigger) {
      this._addPropertyEffect(property, 'annotatedComputation', {
        index: index,
        isCompound: note.isCompound,
        compoundIndex: part.compoundIndex,
        kind: note.kind,
        name: note.name,
        negate: part.negate,
        method: part.signature.method,
        args: part.signature.args,
        trigger: trigger,
        dynamicFn: part.signature.dynamicFn
      });
    },

    // method expressions are of the form: `name([arg1, arg2, .... argn])`
    _parseMethod: function(expression) {
      // tries to match valid javascript property names
      var m = expression.match(/([^\s]+?)\(([\s\S]*)\)/);
      if (m) {
        var sig = { method: m[1], static: true };
        // TODO(kaste): Optimize/memoize `getPropertyInfo`.
        if (this.getPropertyInfo(sig.method) !== Polymer.nob) {
          sig.static = false;
          sig.dynamicFn = true;
        }
        if (m[2].trim()) {
          // replace escaped commas with comma entity, split on un-escaped commas
          var args = m[2].replace(/\\,/g, '&comma;').split(',');
          return this._parseArgs(args, sig);
        } else {
          sig.args = Polymer.nar;
          return sig;
        }
      }
    },

    _parseArgs: function(argList, sig) {
      sig.args = argList.map(function(rawArg) {
        var arg = this._parseArg(rawArg);
        if (!arg.literal) {
          sig.static = false;
        }
        return arg;
      }, this);
      return sig;
    },

    _parseArg: function(rawArg) {
      // clean up whitespace
      var arg = rawArg.trim()
        // replace comma entity with comma
        .replace(/&comma;/g, ',')
        // repair extra escape sequences; note only commas strictly need
        // escaping, but we allow any other char to be escaped since its
        // likely users will do this
        .replace(/\\(.)/g, '\$1')
        ;
      // basic argument descriptor
      var a = {
        name: arg
      };
      // detect literal value (must be String or Number)
      var fc = arg[0];
      if (fc === '-') {
        fc = arg[1];
      }
      if (fc >= '0' && fc <= '9') {
        fc = '#';
      }
      switch(fc) {
        case "'":
        case '"':
          a.value = arg.slice(1, -1);
          a.literal = true;
          break;
        case '#':
          a.value = Number(arg);
          a.literal = true;
          break;
      }
      // if not literal, look for structured path
      if (!a.literal) {
        a.model = Polymer.Path.root(arg);
        // detect structured path (has dots)
        a.structured = Polymer.Path.isDeep(arg);
        if (a.structured) {
          a.wildcard = (arg.slice(-2) == '.*');
          if (a.wildcard) {
            a.name = arg.slice(0, -2);
          }
        }
      }
      return a;
    },

    // instancing
    _marshalInstanceEffects: function() {
      Polymer.Bind.prepareInstance(this);
      if (this._bindListeners) {
        Polymer.Bind.setupBindListeners(this);
      }
    },

    _applyEffectValue: function(info, value) {
      var node = this._nodes[info.index];
      var property = info.name;

      value = this._computeFinalAnnotationValue(node, property, value, info);

      if (info.kind == 'attribute') {
        this.serializeValueToAttribute(value, property, node);
      } else {
        var pinfo = node._propertyInfo && node._propertyInfo[property];
        if (pinfo && pinfo.readOnly) {
          return;
        }
        // Downward data-flow via bindings uses `fromAbove: true` if the
        // global `suppressBindingNotifications` opt-in flag is set as a
        // perf optimization to avoid needless event dispatch cost
        this.__setProperty(property, value,
          Polymer.Settings.suppressBindingNotifications, node);
      }
    },

    _computeFinalAnnotationValue: function(node, property, value, info) {
      if (info.negate) {
        value = !value;
      }

      if (info.isCompound) {
        var storage = node.__compoundStorage__[property];
        storage[info.compoundIndex] = value;
        value = storage.join('');
      }

      if (info.kind !== 'attribute') {
        // TODO(sorvell): consider pre-processing the following two string
        // comparisons in the hot path so this can be a boolean check
        if (property === 'className') {
          value = this._scopeElementClass(node, value);
        }
        // Some browsers serialize `undefined` to `"undefined"`
        if (property === 'textContent' ||
            (node.localName == 'input' && property == 'value')) {
          value = value == undefined ? '' : value;
        }
      }
      return value;
    },

    _executeStaticEffects: function() {
      if (this._propertyEffects && this._propertyEffects.__static__) {
        this._effectEffects('__static__', null, this._propertyEffects.__static__);
      }
    }

  });
</script>
